/*
* Created by boil on 2023/2/22.
*/

#ifndef RENDU_CORE_LOCATOR_LOCATOR_H_
#define RENDU_CORE_LOCATOR_LOCATOR_H_

#include <memory>
#include <utility>
#include "define/define.h"

namespace rendu {

/**
 * @brief Service locator, nothing more.
 *
 * A service locator is used to do what it promises: locate services.<br/>
 * Usually service locators are tightly bound to the services they expose and
 * thus it's hard to define a general purpose class to do that. This tiny class
 * tries to fill the gap and to get rid of the burden of defining a different
 * specific locator for each application.
 *
 * @note
 * Users shouldn't retain references to a service. The recommended way is to
 * retrieve the service implementation currently set each and every time the
 * need for it arises. The risk is to incur in unexpected behaviors otherwise.
 *
 * @tparam Service Service type.
 */
template<typename Service>
class locator final {
  class service_handle {
    friend class locator<Service>;
    std::shared_ptr<Service> value{};
  };

 public:
  /*! @brief Service type. */
  using type = Service;
  /*! @brief Service node type. */
  using node_type = service_handle;

  /*! @brief Default constructor, deleted on purpose. */
  locator() = delete;
  /*! @brief Default destructor, deleted on purpose. */
  ~locator() = delete;

  /**
   * @brief Checks whether a service locator contains a value.
   * @return True if the service locator contains a value, false otherwise.
   */
  [[nodiscard]] static bool has_value() noexcept {
    return (service != nullptr);
  }

  /**
   * @brief Returns a reference to a valid service, if any.
   *
   * @warning
   * Invoking this function can result in undefined behavior if the service
   * hasn't been set yet.
   *
   * @return A reference to the service currently set, if any.
   */
  [[nodiscard]] static Service &value() noexcept {
    RD_ASSERT(has_value(), "Service not available");
    return *service;
  }

  /**
   * @brief Returns a service if available or sets it from a fallback type.
   *
   * Arguments are used only if a service doesn't already exist. In all other
   * cases, they are discarded.
   *
   * @tparam Args Types of arguments to use to construct the fallback service.
   * @tparam Impl Fallback service type.
   * @param args Parameters to use to construct the fallback service.
   * @return A reference to a valid service.
   */
  template<typename Impl = Service, typename... Args>
  [[nodiscard]] static Service &value_or(Args &&...args) {
    return service ? *service : emplace<Impl>(std::forward<Args>(args)...);
  }

  /**
   * @brief Sets or replaces a service.
   * @tparam Impl Service type.
   * @tparam Args Types of arguments to use to construct the service.
   * @param args Parameters to use to construct the service.
   * @return A reference to a valid service.
   */
  template<typename Impl = Service, typename... Args>
  static Service &emplace(Args &&...args) {
    service = std::make_shared<Impl>(std::forward<Args>(args)...);
    return *service;
  }

  /**
   * @brief Sets or replaces a service using a given allocator.
   * @tparam Impl Service type.
   * @tparam Allocator Type of allocator used to manage memory and elements.
   * @tparam Args Types of arguments to use to construct the service.
   * @param alloc The allocator to use.
   * @param args Parameters to use to construct the service.
   * @return A reference to a valid service.
   */
  template<typename Impl = Service, typename Allocator, typename... Args>
  static Service &allocate_emplace(Allocator alloc, Args &&...args) {
    service = std::allocate_shared<Impl>(alloc, std::forward<Args>(args)...);
    return *service;
  }

  /**
   * @brief Returns a handle to the underlying service.
   * @return A handle to the underlying service.
   */
  static node_type handle() noexcept {
    node_type node{};
    node.value = service;
    return node;
  }

  /**
   * @brief Resets or replaces a service.
   * @param other Optional handle with which to replace the service.
   */
  static void reset(const node_type &other = {}) noexcept {
    service = other.value;
  }

 private:
  // std::shared_ptr because of its type erased allocator which is useful here
  inline static std::shared_ptr<Service> service{};
};

} // namespace rendu

#endif //RENDU_CORE_LOCATOR_LOCATOR_H_
